/* ===========================================================
 * TradeManager : An application to trade strategies for the Java(tm) platform
 * ===========================================================
 *
 * (C) Copyright 2011-2011, by Simon Allen and Contributors.
 *
 * Project Info:  org.trade
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 * [Java is a trademark or registered trademark of Oracle, Inc.
 * in the United States and other countries.]
 *
 * (C) Copyright 2011-2011, by Simon Allen and Contributors.
 *
 * Original Author:  Simon Allen;
 * Contributor(s):   -;
 *
 * Changes
 * -------
 *
 */
package org.trade.core.message;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;
import java.util.StringTokenizer;
import java.util.Vector;

import org.trade.core.exception.ExceptionCode;
import org.trade.core.exception.ExceptionMessage;
import org.trade.core.properties.ConfigProperties;
import org.trade.core.properties.PropertyFileNotFoundException;
import org.trade.core.properties.PropertyNotFoundException;

/**
 * This class represnets the data and methods necessary to take exception codes
 * and parameters, and generate the proper exception message based on
 * information contained in a file.
 * 
 * <p>
 * This is the file format:
 * 
 * <p>
 * MY_KEY=Invalid name format: the characters[#bad_chars#] are not allowed <br>
 * MY_KEY_FIELD_REFERENCE=customer_demographic_name <br>
 * MY_KEY_CODE=NAME0802
 * 
 * @author Simon Allen
 */
public class MessageTranslator {
	public final static String NAME_OF_MESSAGE_FILE_IN_PROPERTIES = "ERROR_MESSAGE_FILE_NAME";

	public final static String CODE_SUFFIX = "_CODE";

	public final static String CONTEXT_SUFFIX = "";

	// public final static String PARAMETER_SUFFIX = "_PARAMETER_NAME";
	public final static String FIELD_REFERENCE_SUFFIX = "_FIELD_REFERENCE";

	private static Hashtable<String, MessageFormat> messageFormats = new Hashtable<String, MessageFormat>();

	private static Hashtable<String, String[]> indexesTable = new Hashtable<String, String[]>();

	private static Hashtable<String, String> fieldReferences = new Hashtable<String, String>();

	private static Hashtable<String, String> codes = new Hashtable<String, String>();

	// Constants that map to the keys in the property file
	private PropertyResourceBundle m_props = null;

	private static MessageTranslator m_theConfig = new MessageTranslator();

	// _________________ these methods are taken from the ConfigProperties class
	// ________

	/**
	 * Returns a string for a key.
	 * 
	 * @param key
	 *            String
	 * @return String
	 * @throws IOException
	 */
	public static String getPropAsString(String key) throws IOException {
		String strRet = null;

		strRet = m_theConfig._getProperty(key);

		return strRet;
	}

	// read configuration properties
	/**
	 * Method _getProperty.
	 * 
	 * @param key
	 *            String
	 * @return String
	 * @throws IOException
	 */
	private String _getProperty(String key) throws IOException {
		String ret = null;

		if (null == m_props) {
			InputStream unbuffered;
			unbuffered = getClass().getResourceAsStream(getPropertyFileName());

			if (unbuffered == null) {
				throw new PropertyFileNotFoundException("Check "
						+ "to see if the property file \""
						+ getPropertyFileName()
						+ "\" is installed and available in the class path.");
			} else {
				InputStream in = new BufferedInputStream(unbuffered);
				m_props = new PropertyResourceBundle(in);
				in.close();
				unbuffered.close();
			}
		}

		try {
			ret = m_props.getString(key);
		} catch (MissingResourceException e) {
			throw new PropertyNotFoundException("The property \"" + key
					+ "\" was not found in the property file \""
					+ getPropertyFileName() + "\".  Check the file.");
		}

		return ret;
	}

	/**
	 * this replaces the same method in the ConfigProperties class.
	 * 
	 * @return String
	 */

	public static String getPropertyFileName() {
		try {
			return ConfigProperties
					.getPropAsString(NAME_OF_MESSAGE_FILE_IN_PROPERTIES);
		} catch (Exception e) {
			// default value
			return "messages.properties";
		}
	}

	// ------------------ these methods are not in the ConfigProperties class

	/**
	 * This method takes an exception code and a dictionary, and it uses the
	 * code to get the message, (actual) code and field reference, if it is an
	 * editcheeck
	 * 
	 * @param code
	 *            the Exception code that has the code too look up the message
	 *            by
	 * @param params
	 *            an object that implements the dictionart interface, and
	 *            provides the text to fill in the parameters in the message
	 *            stored in the file
	 * 
	 * @return ExceptionMessage an exception message contatining the proper
	 *         code, field reference, and message * @throws
	 *         MessageTranslatorException
	 */
	public static ExceptionMessage translateExceptionMessage(
			ExceptionCode code, Dictionary<?, ?> params)
			throws MessageTranslatorException {
		return translateExceptionMessage(code.getCode(), params);
	}

	/**
	 * This is the same as the translateExceptionMessage method that takes an
	 * exception code, except it takes a string representing the code to use to
	 * look up the message from the file
	 * 
	 * @param code
	 *            the string to look up the message by
	 * @param params
	 *            an object that implements the dictionart interface, and
	 *            provides the text to fill in the parameters in the message
	 *            stored in the file
	 * 
	 * @return ExceptionMessage an exception message contatining the proper
	 *         code, field reference, and message * @throws
	 *         MessageTranslatorException
	 */
	public static ExceptionMessage translateExceptionMessage(String code,
			Dictionary<?, ?> params) throws MessageTranslatorException {
		// first look up the message + other info based on the code

		MessageFormat mf = lookupMessageFormat(code); // this can throw an
		// exception
		String[] indexNames = lookupArrayIndexNames(code);
		String fieldRef = lookupFieldReference(code);
		String newCode = lookupCodeName(code);

		// next create an object array from the dictionary

		Object[] formatParams = new Object[indexNames.length];
		for (int i = 0; i < indexNames.length; i++) {
			Object param = null;
			if (null != params) {
				param = params.get(indexNames[i]);
			}
			if (null == param) {
				formatParams[i] = "";
			} else {
				formatParams[i] = param;
			}
		}

		// then create the message

		String message = mf.format(formatParams);

		// return an exception message with the gathered values

		if (fieldRef == null) {
			return new ExceptionMessage(new ExceptionCode(newCode), message);
		} else {
			return new ExceptionMessage(new ExceptionCode(newCode, fieldRef),
					message);
		}
	}

	/**
	 * Method retrieveExceptionMessage.
	 * 
	 * @param index
	 *            String
	 * @return ExceptionMessage
	 * @throws MessageTranslatorException
	 */
	public static ExceptionMessage retrieveExceptionMessage(String index)
			throws MessageTranslatorException {
		String code;
		String message;
		String field = null;

		try {
			message = getPropAsString(index);
			code = getPropAsString(index + CODE_SUFFIX);
		} catch (IOException e) {
			throw new MessageTranslatorException(e);
		}

		try {
			field = getPropAsString(index + FIELD_REFERENCE_SUFFIX);
		} catch (PropertyNotFoundException e) {
			// Ignore since the field is optional
		} catch (IOException e) {
			throw new MessageTranslatorException(e);
		}

		ExceptionMessage exceptionMessage;
		exceptionMessage = new ExceptionMessage(new ExceptionCode(code, field),
				message);

		return exceptionMessage;
	}

	/*
	 * public static ExceptionContext retrieveExceptionContext(String index)
	 * throws MessageTranslatorException { String name; String context;
	 * 
	 * try { name = getPropAsString(index + PARAMETER_SUFFIX); context =
	 * getPropAsString(index + CONTEXT_SUFFIX); } catch (IOException e) { throw
	 * new MessageTranslatorException(e); }
	 * 
	 * ExceptionContext exceptionContext; exceptionContext = new
	 * ExceptionContext(name, context);
	 * 
	 * return exceptionContext; }
	 */
	/**
	 * This is the same as the translateExceptionMessage method that takes an
	 * exception code, except it takes an exception message, and extracts the
	 * code from that. because this method takes an exception message as a
	 * parameter, is is able to return the exception message unmodified rather
	 * than throwing an exception
	 * 
	 * 
	 * @param params
	 *            an object that implements the dictionart interface, and
	 *            provides the text to fill in the parameters in the message
	 *            stored in the file
	 * 
	 * @param oldMessage
	 *            ExceptionMessage
	 * @return ExceptionMessage an exception message contatining the proper
	 *         code, field reference, and message
	 */

	public static ExceptionMessage translateExceptionMessage(
			ExceptionMessage oldMessage, Dictionary<?, ?> params) {
		try {
			return translateExceptionMessage(oldMessage.getExceptionCode()
					.getCode(), params);
		} catch (MessageTranslatorException x) {
			return oldMessage;
		}
	}

	/**
	 * Method translateExceptionMessage.
	 * 
	 * @param code
	 *            String
	 * @return ExceptionMessage
	 * @throws MessageTranslatorException
	 */
	public static ExceptionMessage translateExceptionMessage(String code)
			throws MessageTranslatorException {
		return translateExceptionMessage(code, null);
	}

	/**
	 * Method translateExceptionMessage.
	 * 
	 * @param code
	 *            ExceptionCode
	 * @return ExceptionMessage
	 * @throws MessageTranslatorException
	 */
	public static ExceptionMessage translateExceptionMessage(ExceptionCode code)
			throws MessageTranslatorException {
		return translateExceptionMessage(code.getCode(), null);
	}

	/**
	 * Method translateMessage.
	 * 
	 * @param code
	 *            String
	 * @param params
	 *            Dictionary<?,?>
	 * @return String
	 * @throws MessageTranslatorException
	 */
	public static String translateMessage(String code, Dictionary<?, ?> params)
			throws MessageTranslatorException {
		MessageFormat mf = lookupMessageFormat(code); // this can throw an
		// exception
		String[] indexNames = lookupArrayIndexNames(code);

		// next create an object array from the dictionary

		Object[] formatParams = new Object[indexNames.length];
		for (int i = 0; i < indexNames.length; i++) {
			Object param = null;
			if (null != params) {
				param = params.get(indexNames[i]);
			}
			if (null == param) {
				formatParams[i] = "";
			} else {
				formatParams[i] = param;
			}
		}

		// then return the message

		return mf.format(formatParams);
	}

	/**
	 * Method translateMessage.
	 * 
	 * @param code
	 *            String
	 * @return String
	 * @throws MessageTranslatorException
	 */
	public static String translateMessage(String code)
			throws MessageTranslatorException {
		return translateMessage(code, null);
	}

	// look up the message in the hashtable. if it isn't there the try to get
	// all the info
	// for that code from the file
	// if all the info can't be read, throw an exception

	/**
	 * Method lookupMessageFormat.
	 * 
	 * @param code
	 *            String
	 * @return MessageFormat
	 * @throws MessageTranslatorException
	 */
	private static MessageFormat lookupMessageFormat(String code)
			throws MessageTranslatorException {
		MessageFormat mf = messageFormats.get(code);
		if (mf == null) {
			loadMessageFormat(code); // this can throw a translator exception
			mf = messageFormats.get(code);
		}
		return mf;
	}

	// this is the method that actually carries out the loading of the info for
	// the given code

	/**
	 * Method loadMessageFormat.
	 * 
	 * @param code
	 *            String
	 * @throws MessageTranslatorException
	 */
	private static void loadMessageFormat(String code)
			throws MessageTranslatorException {

		try {
			String formatString = getPropAsString(code); // let it throw an
			// exception if not
			// found
			MessageFormat mf = new MessageFormat(formatString);
			String[] indexes = removeNamesAndCreateIndex(mf); // this also
			// strips the
			// named params
			// and replaces
			// then with the
			// numbers that
			// MessageFormat
			// uses
			messageFormats.put(code, mf);
			indexesTable.put(code, indexes);
		} catch (Exception x) {
			throw new MessageTranslatorException(x, x.getMessage());
		}
	}

	/**
	 * Method loadFieldReference.
	 * 
	 * @param code
	 *            String
	 */
	public static void loadFieldReference(String code) {

		try {
			String fieldRef = getPropAsString(code + FIELD_REFERENCE_SUFFIX); // returns
			// null
			// if
			// not
			// found
			if (null != fieldRef) {
				fieldReferences.put(code, fieldRef);
			}
		} catch (Exception e) {
			// it doesn't matter if there isn't a field reference
		}
	}

	/**
	 * Method loadExceptionCode.
	 * 
	 * @param code
	 *            String
	 * @throws MessageTranslatorException
	 */
	public static void loadExceptionCode(String code)
			throws MessageTranslatorException {
		try {
			String newCode = getPropAsString(code + CODE_SUFFIX); // let it
			// throw an
			// exception
			// if not
			// found
			if (null != newCode) {
				codes.put(code, newCode);
			} else {
				// there has to be an exception code, otherwise the exception is
				// not well formed
				throw new Exception("null value for exception code " + code);
			}
		} catch (Exception x) {
			throw new MessageTranslatorException(x,
					"unable to load exception code for " + code);
		}
	}

	// looks up the field reference in the hashtable, returning null if the
	// value is not there

	/**
	 * Method lookupFieldReference.
	 * 
	 * @param code
	 *            String
	 * @return String
	 */
	private static String lookupFieldReference(String code) {
		String fieldRef = fieldReferences.get(code);
		if (null == fieldRef) {
			loadFieldReference(code);
			fieldRef = fieldReferences.get(code);
		}
		return fieldRef;
	}

	// looks up the array index names in the hashtabe for the given code,
	// returning a zero length
	// string array if there are none

	/**
	 * Method lookupArrayIndexNames.
	 * 
	 * @param code
	 *            String
	 * @return String[]
	 */
	private static String[] lookupArrayIndexNames(String code) {
		String[] toReturn = indexesTable.get(code);
		if (null == toReturn) {
			try {
				loadMessageFormat(code); // this can throw a translator
				// exception
				toReturn = indexesTable.get(code);
			} catch (Exception x) {
				toReturn = new String[0];
			}
		}
		return toReturn;
	}

	// looks up

	/**
	 * Method lookupCodeName.
	 * 
	 * @param code
	 *            String
	 * @return String
	 * @throws MessageTranslatorException
	 */
	private static String lookupCodeName(String code)
			throws MessageTranslatorException {
		String newCode = codes.get(code);
		if (null == newCode) {
			loadExceptionCode(code);
			newCode = codes.get(code);
		}
		return newCode;
	}

	// this method takes a message format object that is not valid because it
	// uses
	// #name# instead of {0}, {1}, etc.
	// it replaces each #name# with a number in curly braces
	// it also creates a string array with all of the names that had to be
	// removed

	/**
	 * Method removeNamesAndCreateIndex.
	 * 
	 * @param mf
	 *            MessageFormat
	 * @return String[]
	 */
	private static String[] removeNamesAndCreateIndex(MessageFormat mf) {
		// todo: mabe optimize this to not create a new array each time

		// this code is pasted from the database connection class
		StringTokenizer tokenizer = new StringTokenizer(mf.toPattern(), "#");

		int nbrTokens = tokenizer.countTokens();
		int counter = 0;
		Vector<String> returnVector = new Vector<String>();
		StringBuffer buf = new StringBuffer();

		for (int i = 0; i < nbrTokens; i++)
		// while (nbrTokens-- > 0)
		{
			String token = tokenizer.nextToken();

			if ((i % 2) == 0) // This is not a parameter.
			{
				// no index name to add to the array, nothig to replace in the
				// string
				buf.append(token);

			} else
			// We have a parameter.
			{
				returnVector.addElement(token);

				// Remove the parameter markup and replace it with a number
				// inside curly
				// braces for the MessageFormat to replace with paramaters
				buf.append("{" + counter + "}");
				counter++;
			}
		}
		mf.applyPattern(buf.toString());
		if (returnVector.size() >= 1) {
			String[] namedIndexes = new String[returnVector.size()];
			returnVector.copyInto(namedIndexes);
			return namedIndexes;
		} else {
			return new String[0];
		}
	}

}
